/*
  This file is part of nss-wrtd.

  nss-wrtd is free software; you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published
  by the Free Software Foundation; either version 2 of the License,
  or (at your option) any later version.

  nss-wrtd is distributed in the hope that it will be useful, but1
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with nss-wrtd; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <assert.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <nss.h>
#include <pwd.h>
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

#include "util.h"
#include "avahi.h"
#include "verify-name.h"

#define CONNECTION_USER_PREFIX     "wrtd-connection-level-"
#define CONNECTION_USER_PREFIX_LEN (strlen(CONNECTION_USER_PREFIX))

/* Define prototypes for nss function we're going to export (fixes GCC warnings) */
enum nss_status _nss_wrtd_gethostbyname4_r(const char*, struct gaih_addrtuple**, char*, size_t, int*, int*, int32_t*);
enum nss_status _nss_wrtd_gethostbyname3_r(const char*, int, struct hostent*, char*, size_t, int*, int*, int32_t*, char**);
enum nss_status _nss_wrtd_gethostbyname2_r(const char*, int, struct hostent*, char*, size_t, int*, int*);
enum nss_status _nss_wrtd_gethostbyname_r(const char*, struct hostent*, char*, size_t, int*, int*);
enum nss_status _nss_wrtd_gethostbyaddr2_r(const void*, socklen_t, int, struct hostent*, char*, size_t, int*, int*, int32_t*);
enum nss_status _nss_wrtd_gethostbyaddr_r(const void*, socklen_t, int, struct hostent*, char*, size_t, int*, int*);

static int level_of_current_user() {
    struct passwd* p = getpwuid(geteuid());

    if (strncmp(p->pw_name, CONNECTION_USER_PREFIX, CONNECTION_USER_PREFIX_LEN) == 0) {
        if (*(p->pw_name + CONNECTION_USER_PREFIX_LEN + 1) == '\0') {
            switch (*(p->pw_name + CONNECTION_USER_PREFIX_LEN)) {
            case '1':
                return 1;
            case '2':
                return 2;
            default:
                break;
            }
        }
    }

    return 99;
}

static avahi_resolve_result_t do_avahi_resolve_name(int af, const char* name,
                                                    userdata_t* userdata) {
    bool ipv4_found = false;
    bool ipv6_found = false;

    if (af == AF_INET || af == AF_UNSPEC) {
        query_address_result_t address_result;
        switch (avahi_resolve_name(AF_INET, name, &address_result)) {
        case AVAHI_RESOLVE_RESULT_SUCCESS:
            append_address_to_userdata(&address_result, userdata);
            ipv4_found = true;
            break;

        case AVAHI_RESOLVE_RESULT_HOST_NOT_FOUND:
            break;

        case AVAHI_RESOLVE_RESULT_UNAVAIL:
            /* Something went wrong, just fail. */
            return AVAHI_RESOLVE_RESULT_UNAVAIL;
        }
    }

    if (af == AF_INET6 || af == AF_UNSPEC) {
        query_address_result_t address_result;
        switch (avahi_resolve_name(AF_INET6, name, &address_result)) {
        case AVAHI_RESOLVE_RESULT_SUCCESS:
            append_address_to_userdata(&address_result, userdata);
            ipv6_found = true;
            break;

        case AVAHI_RESOLVE_RESULT_HOST_NOT_FOUND:
            break;

        case AVAHI_RESOLVE_RESULT_UNAVAIL:
            /* Something went wrong, just fail. */
            return AVAHI_RESOLVE_RESULT_UNAVAIL;
        }
    }

    if (ipv4_found || ipv6_found) {
        return AVAHI_RESOLVE_RESULT_SUCCESS;
    } else {
        return AVAHI_RESOLVE_RESULT_HOST_NOT_FOUND;
    }
}

static enum nss_status gethostbyname_impl(const char* name, int af,
                                          userdata_t* u, int* errnop,
                                          int* h_errnop) {

    if (af != AF_INET && af != AF_INET6 && af != AF_UNSPEC) {
        *errnop = EINVAL;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;
    }

    /* FIXME: controls by a switch? */
    name = expand_name(name);

    u->count = 0;

    if (!verify_name_allowed_with_soa(name)) {
        *errnop = EINVAL;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;
    }

    switch (do_avahi_resolve_name(af, name, u)) {
    case AVAHI_RESOLVE_RESULT_SUCCESS:
        return NSS_STATUS_SUCCESS;

    case AVAHI_RESOLVE_RESULT_HOST_NOT_FOUND:
        *errnop = ETIMEDOUT;
        *h_errnop = HOST_NOT_FOUND;
        return NSS_STATUS_NOTFOUND;

    case AVAHI_RESOLVE_RESULT_UNAVAIL:
    default:
        *errnop = ETIMEDOUT;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;
    }
}

enum nss_status _nss_wrtd_gethostbyname4_r(const char* name,
                                           struct gaih_addrtuple** pat,
                                           char* buffer, size_t buflen,
                                           int* errnop, int* h_errnop,
                                           int32_t* ttlp) {

    (void)ttlp;

    enum nss_status status;
    userdata_t u;
    buffer_t buf;

    switch (level_of_current_user()) {
    case 1:
        *errnop = EINVAL;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;
    case 2:
        *errnop = EINVAL;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;          /* FIXME: we should use level-1 calculated nameservers */
    default:
        status = gethostbyname_impl(name, AF_UNSPEC, &u, errnop, h_errnop);
        if (status != NSS_STATUS_SUCCESS) {
            return status;
        }
    }

    buffer_init(&buf, buffer, buflen);
    return convert_userdata_to_addrtuple(&u, name, pat, &buf, errnop, h_errnop);
}

enum nss_status _nss_wrtd_gethostbyname3_r(const char* name, int af,
                                           struct hostent* result, char* buffer,
                                           size_t buflen, int* errnop,
                                           int* h_errnop, int32_t* ttlp,
                                           char** canonp) {

    (void)ttlp;
    (void)canonp;

    enum nss_status status;
    userdata_t u;
    buffer_t buf;

    switch (level_of_current_user()) {
    case 1:
        *errnop = EINVAL;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;
    case 2:
        *errnop = EINVAL;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;          /* FIXME: we should use level-1 calculated nameservers */
    default:
        /* The interfaces for gethostbyname3_r and below do not actually support
           returning results for more than one address family */
        if (af == AF_UNSPEC) {
            af = AF_INET;
        }

        status = gethostbyname_impl(name, af, &u, errnop, h_errnop);
        if (status != NSS_STATUS_SUCCESS) {
            return status;
        }
    }

    buffer_init(&buf, buffer, buflen);
    return convert_userdata_for_name_to_hostent(&u, name, af, result, &buf, errnop, h_errnop);
}

enum nss_status _nss_wrtd_gethostbyname2_r(const char* name, int af,
                                           struct hostent* result,
                                           char* buffer, size_t buflen,
                                           int* errnop, int* h_errnop) {

    return _nss_wrtd_gethostbyname3_r(name, af, result, buffer, buflen, errnop, h_errnop, NULL, NULL);
}

enum nss_status _nss_wrtd_gethostbyname_r(const char* name,
                                          struct hostent* result,
                                          char* buffer, size_t buflen, 
                                          int* errnop, int* h_errnop) {

    return _nss_wrtd_gethostbyname2_r(name, AF_UNSPEC, result, buffer, buflen, errnop, h_errnop);
}

enum nss_status _nss_dns_gethostbyaddr2_r (const void *addr, socklen_t len, int af,
                                           struct hostent *result, char *buffer, size_t buflen,
                                           int *errnop, int *h_errnop, int32_t *ttlp) {

    (void)ttlp;

    size_t address_length;
    char t[256];

    switch (level_of_current_user()) {
    case 1:
        *errnop = EINVAL;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;
    case 2:
        *errnop = EINVAL;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;          /* FIXME: we should use level-1 calculated nameservers */
    }

    /* Check for address types */
    address_length = (af == AF_INET ? sizeof(ipv4_address_t) : sizeof(ipv6_address_t));

    if (len < (int)address_length || (af != AF_INET && af != AF_INET6)) {
        *errnop = EINVAL;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;
    }

    /* Lookup using Avahi */
    buffer_t buf;
    switch (avahi_resolve_address(af, addr, t, sizeof(t))) {
    case AVAHI_RESOLVE_RESULT_SUCCESS:
        buffer_init(&buf, buffer, buflen);
        return convert_name_and_addr_to_hostent(t, addr, address_length, af,
                                                result, &buf, errnop, h_errnop);

    case AVAHI_RESOLVE_RESULT_HOST_NOT_FOUND:
        *errnop = ETIMEDOUT;
        *h_errnop = HOST_NOT_FOUND;
        return NSS_STATUS_NOTFOUND;

    case AVAHI_RESOLVE_RESULT_UNAVAIL:
    default:
        *errnop = ETIMEDOUT;
        *h_errnop = NO_RECOVERY;
        return NSS_STATUS_UNAVAIL;
    }
}

enum nss_status _nss_wrtd_gethostbyaddr_r(const void* addr, socklen_t len, int af,
                                          struct hostent* result, char* buffer, size_t buflen,
                                          int* errnop, int* h_errnop) {

    return _nss_dns_gethostbyaddr2_r(addr, len, af, result, buffer, buflen,
                                      errnop, h_errnop, NULL);
}
